package com.logicbus.backend.message;

import com.alogic.xscript.*;
import com.alogic.xscript.doc.XsObject;
import com.alogic.xscript.log.LogInfo;
import com.alogic.xscript.plugins.Segment;
import com.anysoft.util.IOTools;
import com.anysoft.util.Properties;
import com.anysoft.util.PropertiesConstants;
import com.anysoft.util.Settings;
import com.logicbus.backend.Context;
import com.logicbus.backend.message.tools.ContentDigest;
import com.logicbus.backend.message.tools.GzipTool;
import com.logicbus.backend.message.tools.JsonFactory;
import com.logicbus.backend.server.http.HttpContext;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Server-Sent Events 消息
 *
 * @since 1.6.13.32 [20210128 duanyy]
 *
 * @version 1.6.13.33 [20210203 duanyy] <br>
 * - 支持客户端关闭连接的检测; <br>
 *
 * @version 1.6.15.2 [20211103 duanyy] <br>
 * - 增加Json文档输入 <br>
 */
public class SSEMessage implements Message{
    protected static final Logger LOG = LoggerFactory.getLogger(SSEMessage.class);
    public final static String ID = "$sse";

    public static final String END_OF_MSG = "\n\n";
    public static final String END_OF_LINE = "\n";

    public static final String TYPE_CMD = "cmd";
    public static final String TYPE_MESSAGE = "message";
    public static final String TYPE_DATA = "data";
    public static final String TYPE_RAW = "raw";
    public static final String TYPE_ERROR = "error";

    public static final String CMD_QUIT = "quit";

    public static final String FIELD_DATA = "data";
    public static final String FIELD_EVENT = "event";
    public static final String FIELD_SEPARATOR = ": ";
    public static final String FIELD_ID = "id";

    protected static String formContentType = "application/x-www-form-urlencoded";
    protected static JsonFactory jsonFactory = Settings.getToolkit(JsonFactory.class);
    protected static GzipTool gzipTool = Settings.getToolkit(GzipTool.class);
    protected static ContentDigest contentDigest = Settings.getToolkit(ContentDigest.class);
    static {
        formContentType = PropertiesConstants.getString(Settings.get(),
                "http.formContentType", "application/x-www-form-urlencoded");
    }

    /**
     * Json结构的根节点
     */
    protected Map<String,Object> root = null;
    protected OutputStream out = null;
    protected String contentType = "text/event-stream;charset=utf-8";
    protected String encoding = "utf-8";

    public Map<String,Object> getRoot(){
        return root;
    }

    @Override
    public String toString(){
        return jsonFactory.toJsonString(root);
    }

    @Override
    public void init(Context ctx) {
        boolean gzipEnable = gzipTool.isRequestGzipEnable(ctx);
        String data = null;
        {
            byte [] inputData = ctx.getRequestRaw();
            if (inputData != null){
                try {
                    if (gzipEnable){
                        inputData = gzipTool.decompress(ctx,inputData);
                    }
                    data = new String(inputData,ctx.getEncoding());
                }catch (Exception ex){

                }
            }
        }

        if (data == null){
            //当客户端通过form来post的时候，Message不去读取输入流。
            String _contentType = ctx.getRequestContentType();
            if (_contentType == null || !_contentType.startsWith(formContentType)){
                InputStream in = null;
                try {
                    in = ctx.getInputStream();
                    if (gzipEnable){
                        in = gzipTool.getInputStream(ctx,in);
                    }
                    data = Context.readFromInputStream(in, ctx.getEncoding());
                }catch (Exception ex){
                    LOG.error("Error when reading data from inputstream",ex);
                }finally{
                    IOTools.close(in);
                }
            }
        }

        if (contentDigest != null) {
            contentDigest.digest(ctx, data);
        }

        if (StringUtils.isNotEmpty(data)){
            Object rootObj = jsonFactory.fromJsonString(data);
            if (rootObj instanceof Map){
                root = (Map<String,Object>)rootObj;
            }
        }
        if (root == null){
            root = new HashMap<String,Object>();
        }

        contentType = "text/event-stream;charset=" + ctx.getEncoding();
        encoding = ctx.getEncoding();

        HttpServletResponse httpResponse = ((HttpContext)ctx).getResponse();
        httpResponse.setStatus(HttpServletResponse.SC_OK);
        httpResponse.setHeader("Content-Type",getContentType());
        httpResponse.setHeader("Cache-Control","no-cache");
        httpResponse.setHeader("Connection","keep-alive");

        try {
            out = httpResponse.getOutputStream();
            out.flush();
        }catch (Exception ex){

        }
    }

    @Override
    public void finish(Context ctx, boolean closeStream) {
        writeCommand(CMD_QUIT,false);
        IOTools.close(out);
    }

    @Override
    public String getContentType() {
        return contentType;
    }

    @Override
    public long getContentLength() {
        return 0;
    }

    public boolean writeMessage(String data,boolean flush){
        StringBuffer line = new StringBuffer(FIELD_DATA);
        line.append(FIELD_SEPARATOR).append(escapeCRLF(data)).append(END_OF_MSG);
        return writeRaw(line.toString(),flush);
    }

    public boolean writeCommand(String cmd,boolean flush){
        return writeEvent(TYPE_CMD,cmd,flush);
    }

    public boolean writeEvent(String id,String event,String data,boolean flush){
        StringBuffer line = new StringBuffer();
        line.append(FIELD_ID).append(FIELD_SEPARATOR).append(id).append(END_OF_LINE);
        line.append(FIELD_EVENT).append(FIELD_SEPARATOR).append(event).append(END_OF_LINE);
        line.append(FIELD_DATA).append(FIELD_SEPARATOR).append(escapeCRLF(data)).append(END_OF_MSG);
        return writeRaw(line.toString(),flush);
    }

    public boolean writeEvent(String event,String data,boolean flush){
        StringBuffer line = new StringBuffer();
        line.append(FIELD_EVENT).append(FIELD_SEPARATOR).append(event).append(END_OF_LINE);
        line.append(FIELD_DATA).append(FIELD_SEPARATOR).append(escapeCRLF(data)).append(END_OF_MSG);
        return writeRaw(line.toString(),flush);
    }

    public boolean writeRaw(String raw,boolean flush){
        try {
            out.write(raw.getBytes(encoding));
            if (flush) {
                out.flush();
            }
        }catch (Exception ex){
            return false;
        }
        return true;
    }

    protected static String escapeCRLF(String value){
        return value.replaceAll("\r|\n", "");
    }

    /**
     * 写消息插件
     */
    public static class Writer extends AbstractLogiclet{
        private String pid = ID;
        private String $type = TYPE_MESSAGE;
        private String $data;
        private boolean ref = false;
        private boolean raw = true;
        private String $flush = "true";
        private String id;

        public Writer(String tag, Logiclet p) {
            super(tag, p);
        }

        @Override
        public void configure(Properties p){
            pid = PropertiesConstants.getString(p,"pid",pid,true);
            $type = PropertiesConstants.getRaw(p,"type",$type);
            $data = PropertiesConstants.getRaw(p,"data",$data);
            ref = PropertiesConstants.getBoolean(p,"ref",ref);
            id = PropertiesConstants.getString(p,"id","$" + this.getXmlTag());
            raw = PropertiesConstants.getBoolean(p,"raw",raw);
            $flush = PropertiesConstants.getRaw(p,"flush",$flush);
        }

        @Override
        protected void onExecute(XsObject root, XsObject current, LogicletContext ctx, ExecuteWatcher watcher) {
            SSEMessage msg = ctx.getObject(pid);
            if (msg != null){
                String type = PropertiesConstants.transform(ctx,$type,TYPE_MESSAGE);
                String data = PropertiesConstants.transform(ctx,$data,"");
                if (ref){
                    if (StringUtils.isNotEmpty(data)){
                        data = raw?PropertiesConstants.getRaw(ctx,data,"")
                                :PropertiesConstants.getString(ctx,data,"");
                    }
                }

                if (StringUtils.isNotEmpty(data)){
                    boolean flush = PropertiesConstants.transform(ctx, $flush, true);
                    switch(type){
                        case TYPE_MESSAGE:
                        case TYPE_DATA:
                            ctx.SetValue(id,
                                    BooleanUtils.toStringTrueFalse(
                                            msg.writeMessage(data,flush)));
                            break;
                        case TYPE_CMD:
                            ctx.SetValue(id,
                                    BooleanUtils.toStringTrueFalse(
                                            msg.writeCommand(data, flush)));
                            break;
                        case TYPE_RAW:
                            ctx.SetValue(id,
                                    BooleanUtils.toStringTrueFalse(
                                            msg.writeRaw(data, flush)));
                            break;
                        case TYPE_ERROR:
                        default:
                            ctx.SetValue(id,
                                    BooleanUtils.toStringTrueFalse(
                                            msg.writeEvent(type, data, flush)));
                    }
                }
            }else{
                String type = PropertiesConstants.transform(ctx,$type,TYPE_MESSAGE);
                String data = PropertiesConstants.transform(ctx,$data,"");
                if (ref){
                    if (StringUtils.isNotEmpty(data)){
                        data = raw?PropertiesConstants.getRaw(ctx,data,"")
                                :PropertiesConstants.getString(ctx,data,"");
                    }
                }
                log(data,ctx);
            }
        }
    }

    /**
     * 写消息插件
     */
    public static class Log extends Segment {
        private String pid = ID;
        private boolean flush = true;

        public Log(String tag, Logiclet p) {
            super(tag, p);
        }

        @Override
        public void configure(Properties p){
            pid = PropertiesConstants.getString(p,"pid",pid,true);
            flush = PropertiesConstants.getBoolean(p,"flush",flush,true);
        }

        @Override
        public void log(LogInfo logInfo,Properties ctx){
            if (ctx instanceof LogicletContext){
                LogicletContext logicletContext = (LogicletContext)ctx;
                SSEMessage msg = logicletContext.getObject(pid);
                if (msg != null) {
                    msg.writeMessage(logInfo.message(), flush);
                }else{
                    log(logInfo.message(),logInfo.level(),logInfo.activity());
                }
            }else{
                log(logInfo.message(),logInfo.level(),logInfo.activity());
            }
        }

        @Override
        protected void onExecute(XsObject root,XsObject current, LogicletContext ctx, ExecuteWatcher watcher) {
            LogHandler oldHandler = ctx.getObject(LOG_ID);
            try {
                ctx.setObject(LOG_ID,this);
                super.onExecute(root,current,ctx,watcher);
            }finally {
                ctx.removeObject(LOG_ID);
                if (oldHandler != null){
                    ctx.setObject(LOG_ID,oldHandler);
                }
            }
        }
    }
}
